package relu.R.weeds;
import jfm.utils.*;
import java.util.*;

import org.apache.commons.math.*;
import org.apache.commons.math.stat.descriptive.*;
import org.apache.commons.math.distribution.*;
import relu.R.landscape.*;

public class TransitionMatrix {
	private  double[][] elements;
	private int numBins;
	public double[][] toArray(){
		return elements;
	}
	
	public String toString(){
		StringBuffer buff=new StringBuffer();
		for(int i=0;i<elements.length;i++){
			for(int j=0;j<elements[0].length;j++){
				buff.append(elements[i][j]+",");
			}
			buff.append("\n");
		}
		buff.append("\n");
		return buff.toString();
	}
	
	public static int doTransition(TransitionMatrix tm,int dCategory){
		double[] toWeights=tm.elements[dCategory];
		return JFMMath.chooseWeightedRandom(toWeights, SpatialLandscape.rGen.nextDouble());
	}
	
	
	/** Takes a two column matrix of x ( from crop) and y (to crop) weed density values 
	 * and a set of bins and creates a transition matrix.*/
	public int[] create(double[][] transitions,double[] bins){
		numBins=bins.length;
		elements=new double[bins.length][bins.length];
		int[] numInBins=new int[bins.length];
		// First check the size of transition data
		if ( transitions.length != 2 ){
			throw new Error("Can't create TransitionMatrix from data \n "+
					"Transition data must be of the form double[2][n] \n"+
					"where n is the number of transition points \n");
		}
		ArrayList<Point> points=createSortedPoints(transitions);

		ArrayList<DescriptiveStatistics> dataInBins=new ArrayList<DescriptiveStatistics>();
		for(double d:bins){
			dataInBins.add(new DescriptiveStatistics());
		}
//		System.out.println(points.size());
//		for(int i =0;i<transitions[0].length;i++){
		for(Point p:points){
//			double fromv=transitions[0][i];
//			double tov=transitions[1][i];
			int bin=getBin(p.x,bins);
//			System.out.println("bin "+bin);
			dataInBins.get(bin).addValue(p.y);
//			System.out.println(bin+" "+dataInBins.get(bin).getN()+" "+
//					dataInBins.get(bin).getMean()+dataInBins.get(bin).getVariance());
			
		}
//		for(int b=0;b<bins.length;b++){
//			System.out.println(b+" "+dataInBins.get(b).getN());
//		}

		
		// Condense this to a set of means and variances
		double[] means=new double[bins.length];
		double[] vars = new double[bins.length];
		int numEmptyBins = 0;
		for(int b=0;b<bins.length;b++){
			numInBins[b]=(int)dataInBins.get(b).getN();
			if ( dataInBins.get(b).getN()<3 || ((dataInBins.get(b).getMean() == 0) && (b==0))){// Need at least 3 points in the bin
				means[b]=-dataInBins.get(b).getN();
				vars[b]=-1;
				numEmptyBins++;
			} else {
				means[b]=dataInBins.get(b).getMean();
				vars[b]=dataInBins.get(b).getVariance();
			}
		}
		
		// Check that we have at least one bin with enough data in it
		if ( numEmptyBins == bins.length ){
			// If we have more than 1 empty bin just return the identity matrix
			for( int b=0;b<bins.length; b++){
				for(int bto=0;bto<bins.length;bto++){
					if ( b!=bto){
						elements[b][bto]=0;
					} else {
						elements[b][bto]=1;
					}
				}
				
			}
			normalize();
			return numInBins;
		}
		
		// Put means and variances in for the empties
		for(int b=0;b<bins.length;b++){
			if(means[b]<=0){
				DescriptiveStatistics consData=consolidateDataForBin(b,dataInBins);
//				System.out.println(b+" \n"+consData.toString());
				if ( consData.getN()==0){
					means[b]=0;
					vars[b]=0;
				} else {
					means[b]=consData.getMean();
					vars[b]=consData.getVariance();
				}
//				int nonEmpty=findClosestNonEmptyBin(means,b);
//				int[] consBins=findNearestBinsToConsolidate(dataInBins,b);
//				if ( nonEmpty==-1){ // No data anywhere
//					means[b]=0;
//					vars[b]=0;
//				} else {
//					means[b]=means[nonEmpty];
//					vars[b]=vars[nonEmpty];
//				}
			} else {
				// Do nothing
			}
		}
		try{
		// Now populate the matrix!
		for( int b=0;b<bins.length;b++){
//			System.out.println(means[b]+" "+vars[b]);
			if ( vars[b]==0 && means[b]!=0){
				vars[b]=means[b];
			}
			if ( vars[b]==0 && means[b]==0){
				vars[b]=1;
			}
			NormalDistributionImpl bindist=new NormalDistributionImpl(means[b],vars[b]);
			elements[b][0]=bindist.cumulativeProbability(0, bins[1]);
			for(int bto=1;bto<bins.length;bto++){
				elements[b][bto]=bindist.cumulativeProbability(bins[bto-1], bins[bto]);
			}
		}
		} catch (MathException ex){
			throw new Error(ex.getMessage());
		}
		
		// And finally normalize
		normalize();
		return numInBins;
	}
	
	private void normalize(){
		int nbins=elements[0].length;
		for(int i=0;i<nbins;i++){
			JFMMath.normalize(elements[i]);
		}
	}
	
	private class Point implements Comparable<Point>{
		public final double x;
		public final double y;
		public Point(double x_,double y_){
			x=x_;
			y=y_;			
		}
		
		public int compareTo(Point toObj){
			double comp=x-toObj.x;
			if ( comp < 0 ){return -1;};
			if ( comp == 0 ){return 0;}
			return 1;
		}
		
	}
	
	private ArrayList<Point> createSortedPoints(double[][] transitions){
		ArrayList<Point> points=new ArrayList<Point>();
		for(int i=0;i<transitions[0].length;i++){
			points.add(new Point(transitions[0][i],transitions[1][i]));
		}
		Collections.sort(points);
		return points;
	}
	
	private DescriptiveStatistics consolidateDataForBin(int b,ArrayList<DescriptiveStatistics> dataInBins){
		DescriptiveStatistics upperData=new DescriptiveStatistics();
		DescriptiveStatistics lowerData=new DescriptiveStatistics();
		int nbins=dataInBins.size();
		int numUp=0;
		int numLow=0;
		consol:
		for( int i=b;i<nbins;i++){
			double[] vals=dataInBins.get(i).getValues();
			// These values should retain the order in which they were added.
			// We work backwards adding points until we reach a max of 4
			if ( i==b ){
				for ( double v:vals){
					upperData.addValue(v);
				}
			} else {
			for( int v=vals.length-1;v>=0;v--){
				upperData.addValue(vals[v]);
				if ( upperData.getN()>=3){
					break consol;
				} 
			}
			}
			numUp++;
		}
	
		consol:
		for( int i=b;i>=0;i--){
			double[] vals=dataInBins.get(i).getValues();
			if ( i==b ){
				for ( double v:vals){
					upperData.addValue(v);
				}
			} else {
			for( int v=vals.length-1;v>=0;v--){
				lowerData.addValue(vals[v]);
				if ( lowerData.getN()>=3){
					break consol;
				}
			}
			}
			numLow++;
		}
//		System.out.println(numUp+" numl "+numLow+" "+upperData.getN()+" "+lowerData.getN());
		if ( lowerData.getN() < 3 && upperData.getN() >=3 ){
			return upperData;
		}
		if ( lowerData.getN() >= 3 && upperData.getN() <3 ){
			return lowerData;
		}
		
		if ( numLow == numUp){
			if ( lowerData.getN()>upperData.getN()){				
				return lowerData;
			} else {
				return upperData;
			}
		}
		if ( numLow > numUp ){
			return upperData;
		} else {
			return lowerData;
		}
		
	}
	/*
	private static int findClosestNonEmptyBin(double[] means,int b){
		int nonEmpty=-1;
		boolean foundbig=false;
		boolean foundsml=false;
		int bbig,bsml;
		for( bbig=b;bbig<means.length;bbig++){
			if ( means[bbig]!=-1){
				foundbig=true;
				break;
			}
		}
		for ( bsml=b;bsml>0;bsml--){
			if ( means[bsml]!=-1){
				foundsml=true;
				break;
			}
		}
		
		if ( foundbig && !foundsml){
			return bbig;
		}
		if ( !foundbig && foundsml){
			return bsml;
		}
		if ( foundbig && foundsml){
			int bigd=bbig-b;
			int smld=bsml-b;
			if ( smld==bigd || smld < bigd){
				return bsml;
			} else {
				return bigd;
			}			
		}
		// Didn't find anything
		return nonEmpty;
	}
	
	private static enum BinType{
		LOG,UNIFORM;
	}*/
	
	public static int getBin(double val,double[] bins){
		int bini=0;
		for( double bin:bins){
			if ( val<bin){
				return bini;
			}
			bini++;
		}
		return bini-1;
	}
	/*
	private ArrayList<Double[]> allocateToBins(double[][] transitions,double[] bins){
		double[] xvals=transitions[0];
		ArrayList<Double[]> yinBins=new ArrayList<Double[]>();
		for( )
	}
	*/
	// Try creating bins given a certain min and max 
	/*
	private double[] makeBins(BinType type,double min,double max,int n){
		double[] bins=new double[n];
		switch(type){
		case LOG:
			double maxlog=Math.log(max);
			double[] logbins=JFMMath.seq(min,maxlog,n);
			bins[0]=min;
			for(int i=1;i<n;i++){
				bins[i]=Math.exp(logbins[i]);
			}
			break;
		case UNIFORM:
			bins=JFMMath.seq(min,max,n);
			break;
			default:
				throw new Error("Can't make bins for "+type);
		}
		return bins;
	}*/
	

	
}
